package scan 
{
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.utils.ByteArray;
	import flash.utils.IDataOutput;
	
	/**
	 * Frame sequence metadata keeper
	 */
	public class Metadata 
	{
		////////////////////////////////////////////////////
		// 
		////////////////////////////////////////////////////
		
		public static const TYPE_NONE:String = "none";
		public static const TYPE_XML:String = "xml";
		public static const TYPE_JSON:String = "json";
		public static const TYPE_TEXT:String = "text";
		public static const TYPE_MAP:String = "map";
		
		////////////////////////////////////////////////////
		// 
		////////////////////////////////////////////////////
		
		private var _frames:Vector.<IFrame>;
		
		private var _cropAlpha:Boolean;
		private var _totalSize:Boolean;
		private var _detectIdent:Boolean;
		private var _type:String;
		
		private var _joinFrames:Boolean;
		
		private var _frameRect:Rectangle;
		
		private var _commonWidth:int;
		private var _commonHeight:int;
		
		private var _zeroPoint:Point;
		
		////////////////////////////////////////////////////
		// 
		////////////////////////////////////////////////////
		
		/**
		 * Constructor
		 */
		public function Metadata(frames:Vector.<IFrame>, cropAlpha:Boolean = false, totalSize:Boolean = false,
								 detectIdent:Boolean=false, type:String=null, zeroPoint:Point=null)
		{
			_frames = frames;
			
			_cropAlpha = cropAlpha;
			_totalSize = totalSize;
			_detectIdent = detectIdent;
			
			_zeroPoint = zeroPoint ? zeroPoint.clone() : null;
			
			switch (type)
			{
				case TYPE_XML:
				case TYPE_JSON:
				case TYPE_TEXT:
				case TYPE_MAP:
					_type = type;
					break;
				default:
					_type = TYPE_NONE;
			}
			
			_frameRect = resetFrames(true);
		}
		
		/**
		 * Prepare frames for processing
		 * @param	zeroPosition	set frame position to zero coordinate
		 */
		private function resetFrames(zeroPosition:Boolean=false):Rectangle
		{
			if (_frames.length == 0 ||
				!_cropAlpha && !zeroPosition)
				return new Rectangle();
			
			var allFramesIsEmpty:Boolean = true;
			var frameRc:Rectangle;
			if (_cropAlpha)
			{
				// Find max common frame size
				var l:Number = Number.MAX_VALUE;
				var t:Number = Number.MAX_VALUE;
				var r:Number = Number.MIN_VALUE;
				var b:Number = Number.MIN_VALUE;
				
				for each (var f:IFrame in _frames)
				{
					var frame:Frame = f as Frame;
					if (frame && !frame.frameIsEmpty)
					{
						if (l > frame.cropX)
							l = frame.cropX;
						if (t > frame.cropY)
							t = frame.cropY;
						if (r < frame.cropX + frame.width)
							r = frame.cropX + frame.width;
						if (b < frame.cropY + frame.height)
							b = frame.cropY + frame.height;
						
						allFramesIsEmpty = false;
					}
				}
			}
			else
			{
				for each (f in _frames)
				{
					frame = f as Frame;
					if (frame && !frame.frameIsEmpty)
					{
						frameRc = new Rectangle(0, 0, frame.width, frame.height);
						break;
					}
				}
			}
			
			if (_cropAlpha)
			{
				if (allFramesIsEmpty)
					return new Rectangle();
				else
					frameRc = _cropAlpha ? new Rectangle(l, t, r - l, b - t) : null;
			}
			
			for each (f in _frames)
			{
				frame = f as Frame;
				if (frame)
				{
					frame.doubler = null;
					
					if (zeroPosition)
					{
						frame.x = 0;
						frame.y = 0;
					}
				}
			}
			
			_commonWidth = 0;
			_commonHeight = 0;
			
			return frameRc ? frameRc : new Rectangle();
		}
		
		/**
		 * Get metadata file extention
		 */
		public function get metadataExtention():String
		{
			switch (_type)
			{
				case TYPE_XML: return "xml";
				case TYPE_JSON: return "json";
				case TYPE_TEXT: return "txt";
				case TYPE_MAP: return "jpg";
			}
			return "tmp";
		}
		
		/**
		 * Get metadata for current frames
		 */
		public function writeData(stream:IDataOutput):Boolean
		{
			switch (_type)
			{
				case TYPE_XML:
					stream.writeUTFBytes(createXML());
					break;
				case TYPE_JSON:
					stream.writeUTFBytes(createJSON());
					break;
				case TYPE_TEXT:
					stream.writeUTFBytes(createText());
					break;
				case TYPE_MAP:
				default:
					return false;
			}
			return true;
		}
		
		/**
		 * Frames
		 */
		public function get frames():Vector.<IFrame>
		{
			return _frames;
		}
		
		/**
		 * Common rectangle for all frames
		 */
		public function get frameRect():Rectangle
		{
			return _frameRect;
		}
		
		/**
		 * Type of generated metadata
		 */
		public function get type():String
		{
			return _type;
		}
		
		/**
		 * Keep common size for all saved frames
		 */
		public function get totalSize():Boolean
		{
			return _totalSize;
		}
		
		/**
		 * Crop alpha to minimize frame size
		 */
		public function get cropAlpha():Boolean
		{
			return _cropAlpha;
		}
		
		/**
		 * Total width for the combined frames
		 */
		public function get commonWidth():int
		{
			return _commonWidth;
		}
		
		/**
		 * Total height for the combined frames
		 */
		public function get commonHeight():int
		{
			return _commonHeight;
		}
		
		/**
		 * Join frames in one bitmap, limited with 2048 pixels by width and height
		 */
		public function set joinFrames(value:Boolean):void
		{
			if (value == _joinFrames)
				return;
			
			_joinFrames = value;
			
			if (_frames.length == 0)
				return;
			
			if (_detectIdent)
				findDuplicates();
			
			if (_joinFrames)
			{
				if (_totalSize || !_cropAlpha)
				{
					// All frames has the same size and aligned on a grid
					
					_commonWidth = Space.min(_frameRect.width, Group.WIDTH_LIMIT);
					_commonHeight = Space.min(_frameRect.height, Group.HEIGHT_LIMIT);
					
					// Align
					var row:int = 0;
					var col:int = 0;
					var outOfLimit:Boolean = _frameRect.height > Group.HEIGHT_LIMIT;
					for (var i:int = 0; i < _frames.length; i++)
					{
						var f:IFrame = _frames[i];
						var frame:Frame = f as Frame;
						
						if (frame && !outOfLimit && !frame.frameIsEmpty && !frame.doubler)
						{
							var x:int = col * _frameRect.width;
							var y:int = row * _frameRect.height;
							
							if (x + _frameRect.width > Group.WIDTH_LIMIT)
							{
								_commonWidth = Space.max(x, _commonWidth);
								
								col = 1;
								row++;
								
								x = 0;
								y = row * _frameRect.height;
								
								if (y + _frameRect.height > Group.HEIGHT_LIMIT)
								{
									outOfLimit = true;
									_commonHeight = Group.HEIGHT_LIMIT;
									
									frame.x = 0;
									frame.y = 0;
									frame.inactive = true;
									continue;
								}
								else
								{
									_commonHeight = (row + 1) * _frameRect.height;
								}
							}
							else
							{
								col++;
								_commonWidth = Space.max(col * _frameRect.width, _commonWidth);
							}
							
							frame.x = x;
							frame.y = y;
							frame.inactive = false;
						}
						else
						{
							f.x = 0;
							f.y = 0;
							f.inactive = true;
						}
					}
					
					if (outOfLimit)
						throw Error("Resulting image exceeds the limit of 2048x2048 px. Some frames are lost.");
				}
				else
				{
					// Compose cropped images to minimize the size
					
					var g:Group = layFrames(frames);
					
					_commonWidth = g.width;
					_commonHeight = g.height;
				}
			}
			else
			{
				// Each frame in separated file
				for each (f in _frames)
				{
					f.x = 0;
					f.y = 0;
					f.inactive = false;
				}
				
				_commonWidth = _frameRect.width;
				_commonHeight = _frameRect.height;
			}
		}
		
		/**
		 * Calculate the position of the frames with the optimization
		 * @param	frames	frames to lay
		 * @return	result, the group with sorted frames
		 */
		private function layFrames(frames:Vector.<IFrame>):Group
		{
			if (frames.length == 0)
				return null;
			
			var sortedFrames:Vector.<IFrame> = new Vector.<IFrame>();
			for each (var frame:Frame in frames)
			{
				if (!frame.frameIsEmpty && !frame.doubler)
					sortedFrames.push(frame);
			}
			
			var sortByVolume:Function = function (a:IFrame, b:IFrame):int {
				if (a.volume > b.volume) return -1;
				if (a.volume < b.volume) return 1;
				return 0;
			};
			sortedFrames.sort(sortByVolume);
			
			var g:Group = new Group(new <IFrame>[ sortedFrames.shift() ]);
			while (sortedFrames.length > 0)
			{
				var spaceWasFinded:Boolean = false;
				
				for (var i:int = 0; i < sortedFrames.length; i++)
				{
					var f:IFrame = sortedFrames[i];
					var s:Space = g.getSpaceForFrame(f);
					
					if (s)
					{
						sortedFrames.splice(i, 1);
						if (!g.insert(f, s))
						{
							processGroup(g);
							g = new Group(new <IFrame>[ f ]);
						}
						spaceWasFinded = true;
						break;
					}
				}
				
				if (!spaceWasFinded)
				{
					f = sortedFrames.shift();
					if (!g.insert(f))
					{
						processGroup(g);
						g = new Group(new <IFrame>[ f ]);
					}
				}
			}
			
			return g;
		}
		
		/**
		 * Process the complete group of frames
		 * @param	group
		 */
		private function processGroup(group:Group):void
		{
			// Process result if few files will be supported
			throw Error("Resulting image exceeds the limit of 2048x2048 px. Some frames are lost.");
		}
		
		/**
		 * Find duplicate frames
		 */
		private function findDuplicates():void
		{
			for (var i:int = 0; i < _frames.length; i++)
			{
				var frame:Frame = _frames[i] as Frame;
				
				for (var j:int = 0; j < i; j++)
				{
					var doubler:Frame = _frames[j] as Frame;
					if (doubler && !doubler.doubler)
					{
						if (frame.checksumm == doubler.checksumm)
							frame.doubler = doubler;
					}
				}
			}
		}
		
		/**
		 * Join frames flag
		 */
		public function get joinFrames():Boolean
		{
			return _joinFrames;
		}
		
		/**
		 * Make XML description for current frame sequence
		 * @return result for write in stream
		 */
		private function createXML():String
		{
			var data:XML = new XML('<sequence/>');
			data.@width = commonWidth || _frameRect.width;
			data.@height = commonHeight || _frameRect.height;
			data.@total_frames = _frames.length;
			data.@frame_width = _frameRect.width;
			data.@frame_height = _frameRect.height;
			
			if (_zeroPoint)
			{
				data.@zero_x = _zeroPoint.x - _frameRect.left;
				data.@zero_y = _zeroPoint.y - _frameRect.top;
			}
			else
			{
				data.@zero_x = 0;
				data.@zero_y = 0;
			}
			
			for each (var f:IFrame in _frames)
			{
				var frame:Frame = f as Frame;
				
				if (frame)
				{
					var frameNode:XML = new XML('<frame/>');
					frameNode.@index = frame.index;
					
					if (frame.frameIsEmpty)
					{
						frameNode.@empty = true;
					}
					else
					{
						if (frame.doubler)
							frame = frame.doubler;
						
						frameNode.@file_name = frame.fileName;
						frameNode.@offset_x = frame.cropX - _frameRect.left;
						frameNode.@offset_y = frame.cropY - _frameRect.top;
						
						if (_joinFrames)
						{
							frameNode.@x = frame.x;
							frameNode.@y = frame.y;
						}
						else
						{
							frameNode.@x = 0;
							frameNode.@y = 0;
						}
						
						frameNode.@width = frame.cropWidth;
						frameNode.@height = frame.cropHeight;
					}
					
					data.appendChild(frameNode);
				}
			}
			
			return data.toXMLString();
		}
		
		/**
		 * Make JSON description for current frame sequence
		 * @return result for write in stream
		 */
		private function createJSON():String
		{
			var data:Object = new Object();
			data.width = commonWidth || _frameRect.width;
			data.height = commonHeight || _frameRect.height;
			data.total_frames = _frames.length;
			data.frame_width = _frameRect.width;
			data.frame_height = _frameRect.height;
			
			if (_zeroPoint)
			{
				data.zero_x = _zeroPoint.x - _frameRect.left;
				data.zero_y = _zeroPoint.y - _frameRect.top;
			}
			else
			{
				data.zero_x = 0;
				data.zero_y = 0;
			}
			
			data.frames = [];
			
			for each (var f:IFrame in _frames)
			{
				var frame:Frame = f as Frame;
				
				if (frame)
				{
					var frameNode:Object = new Object();
					frameNode.index = frame.index;
					
					if (frame.frameIsEmpty)
					{
						frameNode.empty = true;
					}
					else
					{
						if (frame.doubler)
							frame = frame.doubler;
						
						frameNode.file_name = frame.fileName;
						frameNode.offset_x = frame.cropX - _frameRect.left;
						frameNode.offset_y = frame.cropY - _frameRect.top;
						
						if (_joinFrames)
						{
							frameNode.x = frame.x;
							frameNode.y = frame.y;
						}
						else
						{
							frameNode.x = 0;
							frameNode.y = 0;
						}
						
						frameNode.width = frame.cropWidth;
						frameNode.height = frame.cropHeight;
					}
					
					data.frames.push(frameNode);
				}
			}
			
			return JSON.stringify(data);
		}
		
		/**
		 * Make text description for current frame sequence
		 * @return result for write in stream
		 */
		private function createText():String
		{
			return "";
		}
		
		/**
		 * Make place map for current frame sequence
		 * @return result for write in stream
		 */
		private function createMap():ByteArray
		{
			return new ByteArray();
		}
	}
}